#pragma once
#include <Poco/Util/HelpFormatter.h>

#include <list>

#include "Common/Types.h"
#include "MServer/MySQL/MySQLPacket.h"

namespace keeper_server {

const unsigned char PROTOCOL_VERSION = 10;
const char *const SERVER_VERSION = "5.7.27-log";

/* Please make sure this is the same as in mysql_com.h */
const UInt32 CLIENT_LONG_PASSWORD = (1UL << 0); /* new more secure passwords */
const UInt32 CLIENT_FOUND_ROWS =
    (1UL << 1);                             /* Found instead of affected rows */
const UInt32 CLIENT_LONG_FLAG = (1UL << 2); /* Get all column flags */
const UInt32 CLIENT_CONNECT_WITH_DB =
    (1UL << 3); /* One can specify db on connect */
const UInt32 CLIENT_NO_SCHEMA =
    (1UL << 4); /* Don't allow database.table.column */
const UInt32 CLIENT_COMPRESS = (1UL << 5);    /* Can use compression protocol */
const UInt32 CLIENT_ODBC = (1UL << 6);        /* Odbc client */
const UInt32 CLIENT_LOCAL_FILES = (1UL << 7); /* Can use LOAD DATA LOCAL */
const UInt32 CLIENT_IGNORE_SPACE = (1UL << 8); /* Ignore spaces before '(' */
const UInt32 CLIENT_PROTOCOL_41 = (1UL << 9);  /* New 4.1 protocol */
const UInt32 CLIENT_INTERACTIVE =
    (1UL << 10);                       /* This is an interactive client */
const UInt32 CLIENT_SSL = (1UL << 11); /* Switch to SSL after handshake */
const UInt32 CLIENT_IGNORE_SIGPIPE = (1UL << 12); /* IGNORE sigpipes */
const UInt32 CLIENT_TRANSACTIONS =
    (1UL << 13); /* Client knows about transactions */
const UInt32 CLIENT_RESERVED = (1UL << 14); /* Old flag for 4.1 protocol  */
const UInt32 CLIENT_SECURE_CONNECTION =
    (1UL << 15); /* New 4.1 authentication */
const UInt32 CLIENT_MULTI_STATEMENTS =
    (1UL << 16); /* Enable/disable multi-stmt support */
const UInt32 CLIENT_MULTI_RESULTS =
    (1UL << 17); /* Enable/disable multi-results */
const UInt32 CLIENT_PS_MULTI_RESULTS =
    (1UL << 18); /* Multi-results in PS-protocol */
const UInt32 CLIENT_PLUGIN_AUTH =
    (1UL << 19); /* Client supports plugin authentication */
const UInt32 CLIENT_CONNECT_ATTRS =
    (1UL << 20); /* Client supports connection attributes */
const UInt32 CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA =
    (1UL << 21); /* for Client: Length of auth response data in
                    Protocol::HandshakeResponse41 is a length-encoded integer.
                    For Server: Understands length-encoded integer for auth
                    response data in Protocol::HandshakeResponse41 */
const UInt32 CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS =
    (1UL << 22); /* for Client: Can handle expired passwords. For Server:
                    Announces support for expired password extension. */
const UInt32 CLIENT_SESSION_TRACK =
    (1UL << 23); /* for Client: Expects the server to send sesson-state changes
                    after a OK packet. For: Server Can set
                    SERVER_SESSION_STATE_CHANGED in the Status Flags and send
                    session-state change data after a OK packet. */
const UInt32 CLIENT_DEPRECATE_EOF =
    (1UL << 24); /* For Client: Expects an OK (instead of EOF) after the
                    resultset rows of a Text Resultset. For Server: Can send OK
                    after a Text Resultset.  */
const UInt32 CLIENT_QUERY_ATTRIBUTE =
    (1UL << 27); /* As of MySQL 8.0.23, clients can also define query attributes
    that apply to the next statement sent to the server for execution. */
const UInt32 CLIENT_SSL_VERIFY_SERVER_CERT =
    (1UL << 30); /* SSL verify server certification */
const UInt32 CLIENT_REMEMBER_OPTIONS =
    (1UL << 31); /* Remember client options */

const UInt32 CLIENT_ALL_FLAGS =
    (CLIENT_LONG_PASSWORD | CLIENT_FOUND_ROWS | CLIENT_LONG_FLAG |
     CLIENT_CONNECT_WITH_DB | CLIENT_NO_SCHEMA | CLIENT_COMPRESS | CLIENT_ODBC |
     CLIENT_LOCAL_FILES | CLIENT_IGNORE_SPACE | CLIENT_PROTOCOL_41 |
     CLIENT_INTERACTIVE | CLIENT_SSL | CLIENT_IGNORE_SIGPIPE |
     CLIENT_TRANSACTIONS | CLIENT_RESERVED | CLIENT_SECURE_CONNECTION |
     CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS | CLIENT_PS_MULTI_RESULTS |
     CLIENT_SSL_VERIFY_SERVER_CERT | CLIENT_REMEMBER_OPTIONS |
     CLIENT_CONNECT_ATTRS | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA |
     CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS | CLIENT_PLUGIN_AUTH);

/*
  Switch off the flags that are optional and depending on build flags
  If any of the optional flags is supported by the build it will be switched
  on before sending to the client during the connection handshake.
*/
const UInt32 CLIENT_BASIC_FLAGS =
    (((CLIENT_ALL_FLAGS & ~CLIENT_SSL) & ~CLIENT_COMPRESS) &
     ~CLIENT_SSL_VERIFY_SERVER_CERT);

const UInt8 SERVER_SCRAMBLE_LEN = 20;

const UInt8 SERVER_CHARSET = 33;
const UInt8 MYSQL_PACKET_HEAD_SIZE = 4;

typedef enum {
  MYSQL_COM_SLEEP,
  MYSQL_COM_QUIT,
  MYSQL_COM_INIT_DB,
  MYSQL_COM_QUERY,
  MYSQL_COM_FIELD_LIST,
  MYSQL_COM_CREATE_DB,
  MYSQL_COM_DROP_DB,
  MYSQL_COM_REFRESH,
  MYSQL_COM_SHUTDOWN,
  MYSQL_COM_STATISTICS,
  MYSQL_COM_PROCESS_INFO,
  MYSQL_COM_CONNECT,
  MYSQL_COM_PROCESS_KILL,
  MYSQL_COM_DEBUG,
  MYSQL_COM_PING,
  MYSQL_COM_TIME,
  MYSQL_COM_DELAYED_INSERT,
  MYSQL_COM_CHANGE_USER,
  MYSQL_COM_BINLOG_DUMP,
  MYSQL_COM_TABLE_DUMP,
  MYSQL_COM_CONNECT_OUT,
  MYSQL_COM_REGISTER_SLAVE,
  MYSQL_COM_PREPARE,
  MYSQL_COM_EXECUTE,
  MYSQL_COM_LONG_DATA,
  MYSQL_COM_CLOSE_STMT,
  MYSQL_COM_RESET_STMT,
  MYSQL_COM_SET_OPTION,
  MYSQL_COM_FETCH_STMT,
  MYSQL_COM_DAEMON,
  MYSQL_COM_BINLOG_DUMP_GTID,
  MYSQL_COM_LAST  // The place holder for the last command number
} MySQLCommand;

enum MySQLServerStatus {
  SERVER_STATUS_NONE,
  SERVER_STATUS_IN_TRANS,
  SERVER_STATUS_AUTOCOMMIT,
  SERVER_MORE_RESULTS_EXISTS,
  SERVER_QUERY_NO_GOOD_INDEX_USED,
  SERVER_QUERY_NO_INDEX_USED,
  SERVER_STATUS_CURSOR_EXISTS,
  SERVER_STATUS_LAST_ROW_SENT,
  SERVER_STATUS_DB_DROPPED,
  SERVER_STATUS_NO_BACKSLASH_ESCAPES,
  SERVER_STATUS_METADATA_CHANGED,
  SERVER_QUERY_WAS_SLOW,
  SERVER_PS_OUT_PARAMS,
  SERVER_STATUS_IN_TRANS_READONLY,
  SERVER_SESSION_STATE_CHANGED
};

/* column defines */

#define NOT_NULL_FLAG 1
#define PRI_KEY_FLAG 2
#define UNIQUE_KEY_FLAG 4
#define MULTIPLE_KEY_FLAG 8
#define BLOB_FLAG 16
#define UNSIGNED_FLAG 32
#define ZEROFILL_FLAG 64
#define BINARY_FLAG 128
#define ENUM_FLAG 256
#define AUTO_INCREMENT_FLAG 512
#define TIMESTAMP_FLAG 1024
#define SET_FLAG 2048
#define NO_DEFAULT_VALUE_FLAG 4096
#define ON_UPDATE_NOW_FLAG 8192
#define NUM_FLAG 32768
#define PART_KEY_FLAG 16384
#define GROUP_FLAG 32768
#define UNIQUE_FLAG 65536
#define BINCMP_FLAG 131072
#define GET_FIXED_FIELDS_FLAG (1 << 18)
#define FIELD_IN_PART_FUNC_FLAG (1 << 19)
#define FIELD_IN_ADD_INDEX (1 << 20)
#define FIELD_IS_RENAMED (1 << 21)
#define FIELD_FLAGS_STORAGE_MEDIA 22
#define FIELD_FLAGS_STORAGE_MEDIA_MASK (3 << FIELD_FLAGS_STORAGE_MEDIA)
#define FIELD_FLAGS_COLUMN_FORMAT 24
#define FIELD_FLAGS_COLUMN_FORMAT_MASK (3 << FIELD_FLAGS_COLUMN_FORMAT)
#define FIELD_IS_DROPPED (1 << 26)
#define EXPLICIT_NULL_FLAG (1 << 27)
#define NOT_SECONDARY_FLAG (1 << 29)
#define FIELD_IS_INVISIBLE (1 << 30)

typedef enum {
  MYSQL_TYPE_DECIMAL,
  MYSQL_TYPE_TINY,
  MYSQL_TYPE_SHORT,
  MYSQL_TYPE_LONG,
  MYSQL_TYPE_FLOAT,
  MYSQL_TYPE_DOUBLE,
  MYSQL_TYPE_NULL,
  MYSQL_TYPE_TIMESTAMP,
  MYSQL_TYPE_LONGLONG,
  MYSQL_TYPE_INT24,
  MYSQL_TYPE_DATE,
  MYSQL_TYPE_TIME,
  MYSQL_TYPE_DATETIME,
  MYSQL_TYPE_YEAR,
  MYSQL_TYPE_NEWDATE,
  MYSQL_TYPE_VARCHAR,
  MYSQL_TYPE_BIT,
  MYSQL_TYPE_TIMESTAMP2,
  MYSQL_TYPE_DATETIME2,
  MYSQL_TYPE_TIME2,
  MYSQL_TYPE_JSON = 245,
  MYSQL_TYPE_NEWDECIMAL = 246,
  MYSQL_TYPE_ENUM = 247,
  MYSQL_TYPE_SET = 248,
  MYSQL_TYPE_TINY_BLOB = 249,
  MYSQL_TYPE_MEDIUM_BLOB = 250,
  MYSQL_TYPE_LONG_BLOB = 251,
  MYSQL_TYPE_BLOB = 252,
  MYSQL_TYPE_VAR_STRING = 253,
  MYSQL_TYPE_STRING = 254,
  MYSQL_TYPE_GEOMETRY = 255,
  MYSQL_TYPE_END
} MySQLColumnType;

extern "C" {
inline void generate_user_salt(char *buffer, int buffer_len) {
  char *end = buffer + buffer_len - 1;

  /* Sequence must be a legal UTF8 string */
  for (; buffer < end; buffer++) {
    *buffer &= 0x7f;
    if (*buffer == '\0' || *buffer == '$') *buffer = *buffer + 1;
  }
  /* Make sure the buffer is terminated properly */
  *end = '\0';
}
}

class MySQLPacketHead {
 public:
  MySQLPacketHead() : payload_len(0), seq_id(0) {}
  ~MySQLPacketHead() {}
  void setPayloadLen(UInt32 value) { payload_len = value; }
  void setSeqID(UInt8 value) { seq_id = value; }
  void pack(MySQLPacket *packet) {
    packet->fill('\0', MYSQL_PACKET_HEAD_SIZE);
    packBody(packet);
    LOG_INFO("body len:%d", packet->length());
    setPayloadLen(packet->length() - MYSQL_PACKET_HEAD_SIZE);
    packHead(packet);
  }

  void packHead(MySQLPacket *packet) {
    LOG_TRACE("Get payload len:%d, %o", payload_len, payload_len);
    memcpy(packet->buff, &payload_len, 3);
    memcpy(packet->buff + 3, &seq_id, 1);
  }

  void unpack(MySQLPacket *packet) {
    unpackHead(packet);
    unpackBody(packet);
  }

  void unpackHead(MySQLPacket *packet) {
    packet->unpackUInt24(&payload_len);
    packet->unpackUInt8(&seq_id);
    LOG_TRACE("unpack with payload:%d", payload_len);
  }

 protected:
  virtual void packBody(MySQLPacket *packet) = 0;
  virtual void unpackBody(MySQLPacket *packet) = 0;

 protected:
  UInt32 payload_len;
  UInt8 seq_id;
};

class MySQLHandShake : public MySQLPacketHead {
 public:
  MySQLHandShake();
  ~MySQLHandShake() {}

  std::string toString(){
    char tmp[1024];
    sprintf(tmp,
            "proto_version:%d,\n"
            "server_version:%s,\n"
            "thread_id:%d,\n"
            "server_scramble:%s,\n"
            "capability_flags1:0x%x,\n"
            "capability_flags2:0x%x,\n"
            "character_set:%d,\n"
            "status_flags:%d\n"
            "server_scramble_len:%d,\n"
            "auth_plugin_name:%s.",
            proto_version, server_version.c_str(), thread_id, server_scramble,
            (UInt16)capability_flags, (capability_flags >> 16), character_set,
            status_flags, server_scramble_len, auth_plugin_name.c_str());

    return std::string(tmp);
  }

  UInt32 length() {
    auto tmp_len = sizeof(proto_version) + server_version.length() + 1 +
                   sizeof(thread_id) + 8 /* scramble 1 */ + 1 /* filter */ +
                   sizeof(capability_flags) + sizeof(character_set) +
                   sizeof(status_flags) + 1 + 10;
    LOG_TRACE("handshake len1:%d", tmp_len);
    if (capability_flags & CLIENT_PLUGIN_AUTH) {
      tmp_len += (auth_plugin_name.length());
    }
    LOG_TRACE("handshake len2:%d", tmp_len);

    return tmp_len;
  }

 protected:
  void packBody(MySQLPacket *packet) override;
  void unpackBody(MySQLPacket *packet) {}

 private:
  UInt8 proto_version;
  String server_version;
  UInt32 thread_id;
  char server_scramble[SERVER_SCRAMBLE_LEN + 1];
  UInt32 capability_flags;
  UInt8 character_set;
  MySQLServerStatus status_flags;
  UInt8 server_scramble_len;
  String auth_plugin_name;
};

class MySQLHandShakeResponse : public MySQLPacketHead {
 public:
  static const UInt8 FILTER_LENGTH = 23;
  MySQLHandShakeResponse();
  ~MySQLHandShakeResponse() {}

  UInt32 length() {
    UInt32 tmp_len =
        sizeof(client_flag) + 3 /* max_packet_size */ + user_name.length();
    return tmp_len;
  }

  String toString() {
    char tmp[1024];
    sprintf(tmp,
            "client_flag:%x,\n"
            "max_packet_size:%u,\n"
            "character_set:%u,\n"
            "client_scramble:%s,\n"
            "user_name:%s,\n"
            "database:%s,\n"
            "client_auth_plugin:%s.\n",
            client_flag, max_packet_size, character_set, client_scramble,
            user_name.c_str(), database.c_str(), client_auth_plugin.c_str());

    return std::string(tmp);
  }

 protected:
  void packBody(MySQLPacket *packet) override;
  void unpackBody(MySQLPacket *packet) override;

 private:
  UInt32 client_flag;
  UInt32 max_packet_size;
  UInt8 character_set;
  char filter[FILTER_LENGTH];
  char client_scramble[SERVER_SCRAMBLE_LEN];
  String user_name;
  String database;
  String client_auth_plugin;
};

class MySQLOKResponse : public MySQLPacketHead {
 public:
  MySQLOKResponse() {
    affected_rows = 1;
    last_insert_id = 0;
    status_flags = SERVER_STATUS_AUTOCOMMIT;
    warnings = 0;
  }
  ~MySQLOKResponse() {}

  String toString() {
    char tmp[1024];
    sprintf(tmp,
            "affect_rows:%lu,\n"
            "last_insert_id:%lu,\n"
            "status_flags:%u,\n"
            "warnings:%u.",
            affected_rows, last_insert_id, status_flags, warnings);
    return tmp;
  }

  UInt32 length() {
    return 1 + sizeof(affected_rows) + sizeof(last_insert_id) +
           2 /* status_flags */ + sizeof(warnings);
  }

 protected:
  void packBody(MySQLPacket *packet) override;
  void unpackBody(MySQLPacket *packet) override {}

 private:
  UInt64 affected_rows;
  UInt64 last_insert_id;
  MySQLServerStatus status_flags;
  UInt16 warnings;
  String status_info;
  String session_state_info;
};

class MySQLEOFResponse : public MySQLPacketHead {
 public:
  MySQLEOFResponse() {
    affected_rows = 1;
    last_insert_id = 0;
    status_flags = SERVER_STATUS_AUTOCOMMIT;
    warnings = 0;
  }
  ~MySQLEOFResponse() {}

  String toString() {
    char tmp[1024];
    sprintf(tmp,
            "affect_rows:%lu,\n"
            "last_insert_id:%lu,\n"
            "status_flags:%u,\n"
            "warnings:%u.",
            affected_rows, last_insert_id, status_flags, warnings);

    return tmp;
  }

  UInt32 length() {
    return 1 + sizeof(affected_rows) + sizeof(last_insert_id) +
           2 /* status_flags */ + sizeof(warnings);
  }

 protected:
  void packBody(MySQLPacket *packet) override;
  void unpackBody(MySQLPacket *packet) override {}

 private:
  UInt64 affected_rows;
  UInt64 last_insert_id;
  MySQLServerStatus status_flags;
  UInt16 warnings;
};

class MySQLResultSetHeaderResponse : public MySQLPacketHead {
 public:
  MySQLResultSetHeaderResponse() { num_of_field = 0; }
  ~MySQLResultSetHeaderResponse() {}

 protected:
  void packBody(MySQLPacket *packet) override {
    packet->packUInt8(num_of_field);
  }
  void unpackBody(MySQLPacket *packet) override {}

 private:
  UInt8 num_of_field;
};

class MySQLColumnsResponse : public MySQLPacketHead {
 public:
  MySQLColumnsResponse() {
    catalog = "";
    schema = "";
    table = "";
    org_table = "";
    filed_length = 0;
    character_set = 0;
    column_length = 0;
    column_type = MYSQL_TYPE_END;
    column_define_flags = 0;
    decimals = 0;
  }
  ~MySQLColumnsResponse() {}

 protected:
  void packBody(MySQLPacket *packet) override {
    packet->packLenencStr(catalog);
    packet->packLenencStr(schema);
    packet->packLenencStr(table);
    packet->packLenencStr(org_table);
    packet->packLenencStr(name);
    packet->packLenencStr(org_name);
    packet->packLenencInt(filed_length);
    packet->packUInt16(character_set);
    packet->packUInt32(column_length);
    packet->packUInt8(column_type);
    packet->packUInt16(column_define_flags);
    packet->packUInt8(decimals);
  }
  void unpackBody(MySQLPacket *packet) override {}

 private:
  String catalog;
  String schema;
  String table;
  String org_table;
  String name;
  String org_name;
  UInt64 filed_length;
  UInt16 character_set;
  UInt32 column_length;
  MySQLColumnType column_type;
  UInt16 column_define_flags;
  UInt8 decimals;
};

class MySQLRowResponse : public MySQLPacketHead {
 public:
  MySQLRowResponse() { data = NULL; }
  ~MySQLRowResponse() {}

 protected:
  void packBody(MySQLPacket *packet) override {
    for (auto item : row_data) {
      packet->packLenencStr(item);
    }
  }
  void unpackBody(MySQLPacket *packet) override {}

 private:
  std::list<String> row_data;
  char *data;
};

}  // namespace keeper_server
